﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;

namespace Aeon.Emulator
{
    /// <summary>
    /// Provides the basic services of an Intel 8237 DMA controller.
    /// </summary>
    public sealed class DmaController : IInputPort, IOutputPort
    {
        private static readonly int[] AllPorts = new int[] { 0x87, 0x00, 0x01, 0x83, 0x02, 0x03, 0x81, 0x04, 0x05, 0x82, 0x06, 0x07, 0x8F, 0xC0, 0xC2, 0x8B, 0xC4, 0xC6, 0x89, 0xC8, 0xCA, 0x8A, 0xCC, 0xCE };

        private const int ModeRegister8 = 0x0B;
        private const int ModeRegister16 = 0xD6;
        private const int MaskRegister8 = 0x0A;
        private const int MaskRegister16 = 0xD4;
        private const int AutoInitFlag = 1 << 4;

        private readonly List<DmaChannel> channels = new List<DmaChannel>(8);
        private VirtualMachine vm;

        /// <summary>
        /// Initializes a new instance of the DmaController class.
        /// </summary>
        internal DmaController(VirtualMachine vm)
        {
            for(int i = 0; i < 8; i++)
            {
                var channel = new DmaChannel();
                this.channels.Add(channel);
            }

            this.Channels = new ReadOnlyCollection<DmaChannel>(channels);
        }

        /// <summary>
        /// Gets the channels on the DMA controller.
        /// </summary>
        public ReadOnlyCollection<DmaChannel> Channels { get; private set; }

        /// <summary>
        /// Gets the input ports implemented by the device.
        /// </summary>
        IEnumerable<int> IInputPort.InputPorts
        {
            get { return Array.AsReadOnly(AllPorts); }
        }
        /// <summary>
        /// Gets the output ports implemented by the device.
        /// </summary>
        IEnumerable<int> IOutputPort.OutputPorts
        {
            get
            {
                var ports = new List<int>(AllPorts);
                ports.Add(ModeRegister8);
                ports.Add(ModeRegister16);
                ports.Add(MaskRegister8);
                ports.Add(MaskRegister16);
                return ports.AsReadOnly();
            }
        }

        /// <summary>
        /// Reads a single byte from one of the device's supported ports.
        /// </summary>
        /// <param name="port">Port from which byte is read.</param>
        /// <returns>
        /// Byte read from the specified port.
        /// </returns>
        byte IInputPort.ReadByte(int port)
        {
            return GetPortValue(port);
        }
        /// <summary>
        /// Reads two bytes from one of the device's supported ports.
        /// </summary>
        /// <param name="port">Port from which bytes are read.</param>
        /// <returns>
        /// Bytes read from the specified port.
        /// </returns>
        ushort IInputPort.ReadWord(int port)
        {
            return GetPortValue(port);
        }
        /// <summary>
        /// Writes a single byte to one of the device's supported ports.
        /// </summary>
        /// <param name="port">Port where byte will be written.</param>
        /// <param name="value">Value to write to the port.</param>
        void IOutputPort.WriteByte(int port, byte value)
        {
            switch(port)
            {
            case ModeRegister8:
                SetChannelMode(channels[value & 3], value);
                break;

            case ModeRegister16:
                SetChannelMode(channels[(value & 3) + 4], value);
                break;

            case MaskRegister8:
                channels[value & 3].IsMasked = (value & 4) != 0;
                break;

            case MaskRegister16:
                channels[(value & 3) + 4].IsMasked = (value & 4) != 0;
                break;

            default:
                SetPortValue(port, value);
                break;
            }
        }
        /// <summary>
        /// Writes two bytes to one or two of the device's supported ports.
        /// </summary>
        /// <param name="port">Port where first byte will be written.</param>
        /// <param name="value">Value to write to the ports.</param>
        void IOutputPort.WriteWord(int port, ushort value)
        {
            int index = Array.IndexOf(AllPorts, port);
            if(index < 0)
                throw new ArgumentException("Invalid port.", "port");

            switch(index % 3)
            {
            case 0:
                channels[index / 3].Page = (byte)value;
                break;

            case 1:
                channels[index / 3].Address = value;
                break;

            case 2:
                channels[index / 3].Count = value;
                channels[index / 3].TransferBytesRemaining = value + 1;
                break;
            }
        }
        /// <summary>
        /// Invoked when the emulator enters a paused state.
        /// </summary>
        void IVirtualDevice.Pause()
        {
        }
        /// <summary>
        /// Invoked when the emulator resumes from a paused state.
        /// </summary>
        void IVirtualDevice.Resume()
        {
        }
        /// <summary>
        /// Invoked when the virtual device has been added to a VirtualMachine.
        /// </summary>
        /// <param name="vm">VirtualMachine which owns the device.</param>
        void IVirtualDevice.DeviceRegistered(VirtualMachine vm)
        {
            this.vm = vm;
        }
        /// <summary>
        /// Invoked when the device should save its current state to a <see cref="System.IO.Stream"/>.
        /// </summary>
        /// <param name="destination"><see cref="System.IO.Stream"/> to which the state is saved.</param>
        void IVirtualDevice.SaveCurrentState(Stream destination)
        {
            var writer = new BinaryWriter(destination);
            foreach(var channel in this.channels)
                channel.Serialize(writer);
        }
        /// <summary>
        /// Invoked when the device should restore its state from a <see cref="System.IO.Stream"/>.
        /// </summary>
        /// <param name="source"><see cref="System.IO.Stream"/> from which the state is restored.</param>
        void IVirtualDevice.RestoreState(Stream source)
        {
            var reader = new BinaryReader(source);
            foreach(var channel in this.channels)
                channel.Deserialize(reader);
        }
        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        void IDisposable.Dispose()
        {
        }

        /// <summary>
        /// Sets DMA channel mode information.
        /// </summary>
        /// <param name="channel">Channel whose mode is to be set.</param>
        /// <param name="value">Flags specifying channel's new mode information.</param>
        private void SetChannelMode(DmaChannel channel, int value)
        {
            if((value & AutoInitFlag) != 0)
                channel.TransferMode = DmaTransferMode.AutoInitialize;
            else
                channel.TransferMode = DmaTransferMode.SingleCycle;
        }
        /// <summary>
        /// Returns the value from a DMA channel port.
        /// </summary>
        /// <param name="port">Port to return value for.</param>
        /// <returns>Value of specified port.</returns>
        private byte GetPortValue(int port)
        {
            int index = Array.IndexOf(AllPorts, port);
            if(index < 0)
                throw new ArgumentException("Invalid port.", "port");

            switch(index % 3)
            {
            case 0:
                return channels[index / 3].Page;

            case 1:
                return channels[index / 3].ReadAddressByte();

            case 2:
                return channels[index / 3].ReadCountByte();
            }

            return 0;
        }
        /// <summary>
        /// Writes a value to a specified DMA channel port.
        /// </summary>
        /// <param name="port">Port to write value to.</param>
        /// <param name="value">Value to write.</param>
        private void SetPortValue(int port, byte value)
        {
            int index = Array.IndexOf(AllPorts, port);
            if(index < 0)
                throw new ArgumentException("Invalid port.", "port");

            switch(index % 3)
            {
            case 0:
                channels[index / 3].Page = value;
                break;

            case 1:
                channels[index / 3].WriteAddressByte(value);
                break;

            case 2:
                channels[index / 3].WriteCountByte(value);
                break;
            }
        }
    }
}
